import satori from "satori"; import { Resvg } from "@resvg/resvg-js"; import { readFileSync } from "node:fs"; const pages: Record = { index: "だれもがしあわせに暮らせるまちへ", jisseki: "実績", policy: "私の方針", support: "ご支援", contact: "コンタクト", "whisper-to-ai-moji-okoshi": "無料・超高精度のWhisper + 生成AIで文字起こしする方法", "koubunsyo-kanri": "公文書管理の不正追及の軌跡", "ijime-judai-jitai": "いじめ重大事態への対応の軌跡", "fukushi-shisetsu-gyakutai": "障害者福祉施設における虐待通報対応の軌跡", "aiki-kouen": "合気公園の軌跡", "joutyo-koteikyu": "情緒固定級の軌跡", "kajo-seigen-kanwa": "過剰な制限緩和の軌跡", "saresio-kaihatu": "東京サレジオ学園北側開発問題の軌跡", "vaccine-kyuusai-tekiseika": "ワクチン副反応救済制度の適正化の軌跡", "dislexia-taiou": "ディスレクシア(読み書き障害)対応の軌跡", "ippan-situmon": "一般質問", }; export async function getStaticPaths() { return Object.keys(pages).map((slug) => ({ params: { slug } })); } const fontBuffer = readFileSync("node_modules/.noto-sans-jp.otf"); const faceiconBuffer = readFileSync("public/img/faceicon.jpg"); const faceiconBase64 = `data:image/jpeg;base64,${faceiconBuffer.toString("base64")}`; // Generate star elements for space-like background const stars = [ { x: 80, y: 40, r: 2, o: 0.8 }, { x: 200, y: 90, r: 1.5, o: 0.6 }, { x: 350, y: 30, r: 2.5, o: 0.9 }, { x: 500, y: 80, r: 1, o: 0.5 }, { x: 650, y: 50, r: 2, o: 0.7 }, { x: 800, y: 100, r: 1.5, o: 0.6 }, { x: 950, y: 40, r: 3, o: 0.85 }, { x: 1100, y: 70, r: 1, o: 0.5 }, { x: 1150, y: 120, r: 2, o: 0.7 }, { x: 150, y: 160, r: 1, o: 0.4 }, { x: 450, y: 140, r: 2, o: 0.5 }, { x: 700, y: 160, r: 1.5, o: 0.45 }, { x: 1000, y: 150, r: 2.5, o: 0.55 }, { x: 50, y: 500, r: 1.5, o: 0.4 }, { x: 300, y: 550, r: 2, o: 0.5 }, { x: 600, y: 520, r: 1, o: 0.35 }, { x: 850, y: 570, r: 2.5, o: 0.5 }, { x: 1050, y: 540, r: 1, o: 0.4 }, { x: 1150, y: 580, r: 1.5, o: 0.45 }, { x: 120, y: 300, r: 1, o: 0.3 }, { x: 1080, y: 280, r: 1.5, o: 0.35 }, ].map((s, i) => ({ type: "div" as const, props: { key: `star-${i}`, style: { position: "absolute" as const, left: `${s.x}px`, top: `${s.y}px`, width: `${s.r * 2}px`, height: `${s.r * 2}px`, borderRadius: "50%", background: `rgba(255,255,255,${s.o})`, boxShadow: `0 0 ${s.r * 3}px rgba(255,255,255,${s.o * 0.4})`, }, }, })); export async function GET({ params }: { params: { slug: string } }) { const slug = params.slug; const title = pages[slug] ?? "小平市議 安竹洋平 公式サイト"; const svg = await satori( { type: "div", props: { style: { display: "flex", width: "1200px", height: "630px", background: "linear-gradient(135deg, #0f0d2e 0%, #1e1b4b 30%, #3730a3 60%, #4f46e5 100%)", color: "#ffffff", fontFamily: "sans-serif", padding: "64px 80px", alignItems: "center", gap: "56px", position: "relative", overflow: "hidden", }, children: [ ...stars, { type: "img", props: { src: faceiconBase64, width: 180, height: 180, style: { borderRadius: "50%", border: "3px solid rgba(255,255,255,0.25)", flexShrink: "0", position: "relative", }, }, }, { type: "div", props: { style: { display: "flex", flexDirection: "column", gap: "20px", flex: "1", position: "relative", }, children: [ { type: "div", props: { style: { fontSize: "48px", fontWeight: "700", lineHeight: "1.2", letterSpacing: "-0.02em", }, children: title, }, }, { type: "div", props: { style: { display: "flex", gap: "14px", fontSize: "24px", fontWeight: "500", color: "#a5b4fc", }, children: [ { type: "div", props: { children: "だれもが" } }, { type: "div", props: { style: { color: "#c7d2fe" }, children: "しあわせに", }, }, { type: "div", props: { style: { color: "#e0e7ff" }, children: "暮らせるまちへ", }, }, ], }, }, { type: "div", props: { style: { display: "flex", gap: "16px", fontSize: "20px", fontWeight: "400", color: "rgba(255,255,255,0.45)", paddingTop: "12px", borderTop: "1px solid rgba(255,255,255,0.1)", }, children: [ { type: "div", props: { children: "小平市議 安竹洋平" } }, { type: "div", props: { style: { opacity: "0.6" }, children: "yasutakeyohei.com", }, }, ], }, }, ], }, }, ], }, } as any, { width: 1200, height: 630, fonts: [ { name: "sans-serif", data: fontBuffer, weight: 400, style: "normal" }, ], }, ); const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } }); const pngBuffer = resvg.render().asPng(); return new Response(pngBuffer, { headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=31536000, immutable", }, }); }